<?php

declare(strict_types=1);

namespace app\service\product;

use app\model\Prod;
use app\model\ProdCategory;
use InvalidArgumentException;
use think\Collection;
use think\facade\Db;
use think\Paginator;

class CategoryService
{
    public function paginate(array $filters, int $page, int $pageSize): Paginator
    {
        $query = ProdCategory::order(['level' => 'asc', 'sort' => 'asc', 'id' => 'asc']);

        if (!empty($filters['keyword'])) {
            $keyword = trim((string) $filters['keyword']);
            $query->where(function ($subQuery) use ($keyword) {
                $subQuery->whereLike('name', '%' . $keyword . '%')
                    ->whereOr('slug', 'like', '%' . $keyword . '%')
                    ->whereOr('description', 'like', '%' . $keyword . '%');
            });
        }

        if (isset($filters['is_visible']) && $filters['is_visible'] !== '') {
            $query->where('is_visible', (int) $filters['is_visible']);
        }

        if (!empty($filters['parent_id'])) {
            $query->where('parent_id', (int) $filters['parent_id']);
        }

        return $query->paginate([
            'list_rows' => $pageSize,
            'page' => $page,
        ]);
    }

    public function tree(array $filters = []): array
    {
        $query = ProdCategory::order(['level' => 'asc', 'sort' => 'asc', 'id' => 'asc']);

        if (isset($filters['is_visible']) && $filters['is_visible'] !== '') {
            $query->where('is_visible', (int) $filters['is_visible']);
        }

        $categories = $query->select();
        $formatted = $this->formatCollection($categories);

        $map = [];
        foreach ($formatted as $item) {
            $map[$item['id']] = $item;
        }

        $parentId = isset($filters['parent_id']) ? (int) $filters['parent_id'] : 0;
        if ($parentId > 0) {
            if (!isset($map[$parentId])) {
                throw new InvalidArgumentException('父分类不存在');
            }
            $parentPath = $map[$parentId]['path'] ?? '/';
            $formatted = array_values(array_filter($formatted, static function (array $item) use ($parentPath) {
                $path = $item['path'] ?? '';
                return strpos($path, $parentPath) === 0;
            }));
            $map = [];
            foreach ($formatted as $item) {
                $map[$item['id']] = $item;
            }
        }

        if (!empty($filters['keyword'])) {
            $keyword = mb_strtolower(trim((string) $filters['keyword']));
            $matchedIds = [];
            foreach ($formatted as $item) {
                $haystack = mb_strtolower(($item['name'] ?? '') . ' ' . ($item['slug'] ?? '') . ' ' . ($item['description'] ?? ''));
                if ($keyword !== '' && str_contains($haystack, $keyword)) {
                    $matchedIds[] = $item['id'];
                }
            }

            if ($matchedIds !== []) {
                $keepIds = [];
                foreach ($matchedIds as $id) {
                    if (!isset($map[$id])) {
                        continue;
                    }
                    $path = $map[$id]['path'] ?? '';
                    $segments = array_filter(explode('/', $path), static fn($segment) => $segment !== '');
                    foreach ($segments as $segment) {
                        $keepIds[(int) $segment] = true;
                    }
                }

                $formatted = array_values(array_filter($formatted, static function (array $item) use ($keepIds) {
                    return isset($keepIds[$item['id']]);
                }));
            } else {
                $formatted = [];
            }
        }

        if ($formatted === []) {
            return [];
        }

        $tree = $this->buildTree($formatted, $parentId > 0 ? $parentId : 0);

        if ($parentId > 0) {
            $parent = $map[$parentId] ?? null;
            if ($parent !== null) {
                $parent['children'] = $tree;
                return [$parent];
            }
        }

        return $tree;
    }

    public function findById(int $id): ?ProdCategory
    {
        return ProdCategory::find($id);
    }

    public function create(array $payload): ProdCategory
    {
        $name = trim((string) ($payload['name'] ?? ''));
        $slug = trim((string) ($payload['slug'] ?? ''));
        $parentId = isset($payload['parent_id']) ? (int) $payload['parent_id'] : 0;

        if ($name === '') {
            throw new InvalidArgumentException('分类名称不能为空');
        }

        if ($slug === '') {
            throw new InvalidArgumentException('分类编码不能为空');
        }

        $this->assertSlugUnique($slug);

        [$level, $basePath] = $this->computeHierarchy($parentId);

        $data = $this->extractPayload($payload);
        $data['name'] = $name;
        $data['slug'] = $slug;
        $data['parent_id'] = $parentId;
        $data['level'] = $level;

        /** @var ProdCategory $category */
        $category = ProdCategory::create($data);
        $category->path = $this->buildOwnPath($basePath, (int) $category->id);
        $category->save();

        return $this->findById((int) $category->id);
    }

    public function update(ProdCategory $category, array $payload): ProdCategory
    {
        $data = $this->extractPayload($payload);

        if (array_key_exists('name', $payload)) {
            $name = trim((string) $payload['name']);
            if ($name === '') {
                throw new InvalidArgumentException('分类名称不能为空');
            }
            $data['name'] = $name;
        }

        if (array_key_exists('slug', $payload)) {
            $slug = trim((string) $payload['slug']);
            if ($slug === '') {
                throw new InvalidArgumentException('分类编码不能为空');
            }
            if ($slug !== $category->slug) {
                $this->assertSlugUnique($slug, (int) $category->id);
            }
            $data['slug'] = $slug;
        }

        $parentChanged = false;
        $parentId = $category->parent_id;
        if (array_key_exists('parent_id', $payload)) {
            $newParentId = (int) $payload['parent_id'];
            if ($newParentId < 0) {
                $newParentId = 0;
            }

            if ($newParentId === $category->id) {
                throw new InvalidArgumentException('父级不能指向自身');
            }

            if ($newParentId !== $category->parent_id) {
                $this->assertNoCycle($category, $newParentId);
                $parentChanged = true;
                $parentId = $newParentId;
                $data['parent_id'] = $newParentId;
            }
        }

        $newBasePath = null;
        if ($parentChanged) {
            [$level, $basePath] = $this->computeHierarchy($parentId);
            $data['level'] = $level;
            $newBasePath = $basePath;
        }

        return Db::transaction(function () use ($category, $data, $parentChanged, $newBasePath) {
            $oldPath = $category->path;
            $category->save($data);

            if ($parentChanged) {
                $category->path = $this->buildOwnPath($newBasePath ?? '/', (int) $category->id);
                $category->save();
                $this->refreshDescendantPath($category, $oldPath);
            }

            return $this->findById((int) $category->id);
        });
    }

    public function delete(ProdCategory $category): bool
    {
        $hasChildren = ProdCategory::where('parent_id', $category->id)->count() > 0;
        if ($hasChildren) {
            throw new InvalidArgumentException('请先删除子分类');
        }

        $productExists = Prod::where('category_id', $category->id)->count() > 0;
        if ($productExists) {
            throw new InvalidArgumentException('该分类仍有关联商品，无法删除');
        }

        return (bool) $category->delete();
    }

    public function format(ProdCategory $category): array
    {
        return [
            'id' => (int) $category->id,
            'name' => $category->name,
            'slug' => $category->slug,
            'parent_id' => (int) $category->parent_id,
            'level' => (int) $category->level,
            'path' => $category->path,
            'icon_url' => $category->icon_url,
            'cover_url' => $category->cover_url,
            'description' => $category->description,
            'content' => $category->content,
            'sort' => (int) $category->sort,
            'is_visible' => (int) $category->is_visible,
            'create_time' => $category->create_time,
            'update_time' => $category->update_time,
        ];
    }

    public function formatCollection(iterable $categories): array
    {
        $results = [];
        foreach ($categories as $category) {
            if ($category instanceof ProdCategory) {
                $results[] = $this->format($category);
            }
        }

        return $results;
    }

    public function buildTree(array $categories, int $parentId = 0): array
    {
        $grouped = [];
        foreach ($categories as $category) {
            $grouped[$category['parent_id']][] = $category;
        }

        $build = function ($pid) use (&$build, $grouped) {
            $children = $grouped[$pid] ?? [];
            foreach ($children as &$item) {
                $item['children'] = $build($item['id']);
                if ($item['children'] === []) {
                    unset($item['children']);
                }
            }
            return $children;
        };

        return $build($parentId);
    }

    private function extractPayload(array $payload): array
    {
        $fields = [
            'icon_url',
            'cover_url',
            'description',
            'content',
            'sort',
            'is_visible',
        ];

        $data = array_intersect_key($payload, array_flip($fields));

        if (array_key_exists('sort', $data)) {
            $data['sort'] = (int) $data['sort'];
        }
        if (array_key_exists('is_visible', $data)) {
            $data['is_visible'] = (int) $data['is_visible'];
        }

        return $data;
    }

    private function computeHierarchy(int $parentId): array
    {
        if ($parentId <= 0) {
            return [1, '/'];
        }

        /** @var ProdCategory|null $parent */
        $parent = ProdCategory::find($parentId);
        if (!$parent) {
            throw new InvalidArgumentException('父分类不存在');
        }

        $level = (int) $parent->level + 1;
        if ($level > 5) {
            throw new InvalidArgumentException('分类层级不能超过5级');
        }

        $basePath = $parent->path ?: '/';

        return [$level, $basePath];
    }

    private function refreshDescendantPath(ProdCategory $category, string $oldPath): void
    {
        $newPath = $category->path;
        if ($oldPath === $newPath) {
            return;
        }

        $descendants = ProdCategory::whereLike('path', $oldPath . '%')
            ->where('id', '<>', $category->id)
            ->select();

        foreach ($descendants as $descendant) {
            $relativePath = substr($descendant->path, strlen($oldPath));
            $descendant->path = $newPath . $relativePath;
            $descendant->level = substr_count($descendant->path, '/') - 1;
            $descendant->save();
        }
    }

    private function assertSlugUnique(string $slug, ?int $excludeId = null): void
    {
        $query = ProdCategory::where('slug', $slug);
        if ($excludeId !== null) {
            $query->where('id', '<>', $excludeId);
        }

        if ($query->count() > 0) {
            throw new InvalidArgumentException('分类编码已存在');
        }
    }

    private function assertNoCycle(ProdCategory $category, int $newParentId): void
    {
        if ($newParentId <= 0) {
            return;
        }

        $parent = ProdCategory::find($newParentId);
        if (!$parent) {
            throw new InvalidArgumentException('父分类不存在');
        }

        if (str_starts_with($parent->path, $category->path)) {
            throw new InvalidArgumentException('父级不能指向其子孙节点');
        }
    }

    private function buildOwnPath(string $basePath, int $id): string
    {
        $normalized = rtrim($basePath, '/') . '/';
        if ($normalized === '//') {
            $normalized = '/';
        }

        if ($normalized === '/' || $normalized === '') {
            return '/' . $id . '/';
        }

        return $normalized . $id . '/';
    }
}
